# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: xguo

import logging
import math

import dateutil.parser

import coin.exchange.base.kr_rest.private_client_base as privcb
import coin.exchange.bitflyer_v1.kr_rest.native_private_client as nprivc
import coin.exchange.bitflyer_v1.kr_rest.futures_product as exchange_product
from coin.exchange.bitflyer_v1.kr_rest.currency import BitflyerCurrency
from coin.exchange.bitflyer_v1.kr_rest.futures_product import BitflyerFuturesProduct
from coin.support.accounting.logic.account_proto_util import (sum_balances)

import coin.proto.coin_order_enums_pb2 as coin_order
from coin.proto.coin_query_pb2 import (CurrencyBalance,
                                       AccountBalance,
                                       MarginUsage,
                                       ProductPosition,
                                       AccountPosition,
                                       ProductFillElement,
                                       ProductFillBundle,
                                       ProductOrderElement,
                                       ProductOrderBundle)


class BitflyerPrivateParser(object):
  @staticmethod
  def parse_balance(json_msg):
    currency_balance_list = []
    for elem in json_msg:
      currency = BitflyerCurrency.FromStrNativeCurrency(elem["currency_code"])
      total = elem['amount']
      available = elem['available']
      assert total >= 0
      assert 0 <= available <= total
      hold = total - available
      currency_balance = CurrencyBalance(
          currency=currency.symbol,
          total=total,
          available=available,
          hold=hold,
      )
      currency_balance_list.append(currency_balance)
    return AccountBalance(
        exchange='Bitflyer',
        each_balance=currency_balance_list,
    )

  @staticmethod
  def parse_balance_in_unrealized_pnl(json_msg):
    unrealized_pnl = 0.0
    for elem in json_msg:
      unrealized_pnl += elem['pnl']
    # Unrealized PNL is treated as hold balance.
    currency_balance = CurrencyBalance(currency='JPY',
                                       total=unrealized_pnl,
                                       available=0,
                                       hold=unrealized_pnl)
    return AccountBalance(
        exchange='Bitflyer',
        each_balance=[currency_balance],
    )

  @staticmethod
  def parse_balance_in_margin_deposit(json_msg):
    """ margin list
    [
      {"currency_code": "JPY", "amount": 10000 },
      {"currency_code": "BTC", "amount": 1.23 }
    ]"""
    currency_balance_list = []
    for elem in json_msg:
      assert elem['amount'] >= 0.0, elem
      currency_balance_list.append(
          CurrencyBalance(
              currency=BitflyerCurrency.FromStr(elem['currency_code']).symbol,
              total=elem['amount'],
              available=0,
              hold=elem['amount'],
          ))
    return AccountBalance(
        exchange='Bitflyer',
        each_balance=currency_balance_list,
    )

  @staticmethod
  def parse_position(json_msg):
    # [{'product_code': 'FX_BTC_JPY', 'side': 'BUY', 'price': 1028699.0, 'size': 0.001, 'commission': 0.0, 'swap_point_accumulate': 0.0, 'require_collateral': 342.8996666666667, 'open_date': '2018-03-10T17:32:12.88', 'leverage': 3.0, 'pnl': -4.799, 'sfd': 0.0}]
    # [{'product_code': 'FX_BTC_JPY', 'side': 'SELL', 'price': 1020406.0, 'size': 0.001, 'commission': 0.0, 'swap_point_accumulate': 0.0, 'require_collateral': 340.13533333333334, 'open_date': '2018-03-10T17:47:28.203', 'leverage': 3.0, 'pnl': -2.823, 'sfd': 0.0}]

    position_map = {}
    for elem in json_msg:
      product = BitflyerFuturesProduct.FromStr(elem["product_code"])
      position = 0.
      entry_price = float(elem["price"])

      if elem["side"] == "BUY":
        position = float(elem["size"])
      elif elem["side"] == "SELL":
        position = -1 * float(elem["size"])
      else:
        raise ValueError()

      if math.isclose(position, 0):
        continue

      if product in position_map:
        prev_pos, prev_price = position_map[product]
        if ((position > 0 and prev_pos < 0) or (position < 0 and prev_pos > 0)):
          logging.error('Side mismatch: %s', str(json_msg))
          avg_entry_price = 0.
        else:
          avg_entry_price = ((prev_pos * prev_price + position * entry_price) /
                             (prev_pos + position))
        position_map[product] = (prev_pos + position, avg_entry_price)
      else:
        position_map[product] = (position, entry_price)

    position_list = []
    for product, position in position_map.items():
      product_position = ProductPosition(symbol=product.symbol,
                                         net_position=position[0],
                                         avg_entry_price=position[1])
      if product.base.symbol == "BTC":
        product_position.net_position_contract = int(position[0] * 1e8)  # in satoshi
      position_list.append(product_position)

    return AccountPosition(
        exchange='Bitflyer',
        each_position=position_list,
    )

  @staticmethod
  def parse_margin(json_msg):
    # Only contains JPY margin usage info.
    total_margin = json_msg['collateral']
    hold_margin = json_msg['require_collateral']
    assert total_margin >= 0
    assert hold_margin >= 0
    available_margin = total_margin - hold_margin
    return MarginUsage(
        currency='JPY',
        total_margin=total_margin,
        hold_margin=hold_margin,
        available_margin=available_margin,
    )

  @staticmethod
  def parse_single_fill(json_msg, product):
    if json_msg['side'] == 'BUY':
      side = coin_order.BUY_ORDER
    elif json_msg['side'] == 'SELL':
      side = coin_order.SELL_ORDER
    else:
      raise ValueError('Invalid side value %s' % json_msg['side'])

    order_id = json_msg['child_order_acceptance_id']
    price = json_msg['price']
    qty = json_msg['size']
    fee = json_msg['commission']
    timestamp = int(dateutil.parser.parse(json_msg['exec_date'] + 'Z').timestamp() * 1e9)
    return ProductFillElement(
        symbol=product.symbol,
        order_id=order_id,
        side=side,
        price=price,
        qty=qty,
        fill_timestamp=timestamp,
        fee=fee,
    )

  @staticmethod
  def parse_fills(json_msg, product):
    product_fill_list = []
    for elem in json_msg:
      product_fill = BitflyerPrivateParser.parse_single_fill(elem, product)
      product_fill_list.append(product_fill)

    return ProductFillBundle(
        exchange='Bitflyer',
        symbol=product.symbol,
        each_fill=product_fill_list,
    )

  @staticmethod
  def parse_single_open_order(json_msg, product):
    if json_msg['side'] == 'BUY':
      side = coin_order.BUY_ORDER
    elif json_msg['side'] == 'SELL':
      side = coin_order.SELL_ORDER
    else:
      raise ValueError('Invalid side value %s' % json_msg['side'])

    order_id = json_msg['child_order_acceptance_id']
    timestamp = int(dateutil.parser.parse(json_msg['child_order_date'] + 'Z').timestamp() * 1e9)
    price = json_msg['price']
    qty = json_msg['size']
    assert price >= 0.0
    assert qty >= 0.0
    return ProductOrderElement(symbol=product.symbol,
                               price=price,
                               qty=qty,
                               side=side,
                               order_id=order_id,
                               created_timestamp=timestamp)

  @staticmethod
  def parse_orders(json_msg, product):
    # {'id': 285693813, 'child_order_id': 'JFX20180401-045018-839980F', 'product_code': 'FX_BTC_JPY', 'side': 'BUY', 'child_order_type': 'LIMIT', 'price': 746831.0, 'average_price': 746787.0, 'size': 0.001, 'child_order_state': 'COMPLETED', 'expire_date': '2018-04-01T05:50:16', 'child_order_date': '2018-04-01T04:50:16', 'child_order_acceptance_id': 'JRF20180401-045016-763613', 'outstanding_size': 0.0, 'cancel_size': 0.0, 'executed_size': 0.001, 'total_commission': 0.0}
    open_order_list = []
    for elem in json_msg:
      open_order = BitflyerPrivateParser.parse_single_open_order(elem, product)
      open_order_list.append(open_order)

    return ProductOrderBundle(
        exchange='Bitflyer',
        symbol=product.symbol,
        each_order=open_order_list,
    )


class BitflyerPrivateClient(privcb.PrivateClientBase):
  ProductType = exchange_product.BitflyerFuturesProduct

  def __init__(self, key_file, *, region="Japan", timeout=20, tps_limiter=None):
    super().__init__()
    self.nprivc = nprivc.BitflyerNativePrivateClient(key_file,
                                                     region=region,
                                                     timeout=timeout,
                                                     tps_limiter=tps_limiter)

  def query_account_balance_impl(self):
    update = self.nprivc.query_account_asset_balance()
    update.msg = BitflyerPrivateParser.parse_balance(update.msg)

    margin_update = self.nprivc.query_margin_status_account()
    margin_deposit = BitflyerPrivateParser.parse_balance_in_margin_deposit(margin_update.msg)
    # only support FX_BTC_JPY now.
    position_update = self.nprivc.query_open_interest_summary("FX_BTC_JPY")
    unrealized_pnl = BitflyerPrivateParser.parse_balance_in_unrealized_pnl(position_update.msg)

    update.msg = sum_balances([update.msg, margin_deposit, unrealized_pnl])
    update.res_timestamp = position_update.res_timestamp
    return update

  def query_account_balance_spot_only_impl(self):
    update = self.nprivc.query_account_asset_balance()
    update.msg = BitflyerPrivateParser.parse_balance(update.msg)
    return update

  def query_account_balance_futures_only_impl(self):
    margin_update = self.nprivc.query_margin_status_account()
    margin_deposit = BitflyerPrivateParser.parse_balance_in_margin_deposit(margin_update.msg)
    # only support FX_BTC_JPY now.
    position_update = self.nprivc.query_open_interest_summary("FX_BTC_JPY")
    unrealized_pnl = BitflyerPrivateParser.parse_balance_in_unrealized_pnl(position_update.msg)

    update = margin_update
    update.msg = sum_balances([margin_deposit, unrealized_pnl])
    update.res_timestamp = position_update.res_timestamp
    return update

  def query_account_position_impl(self):
    # only support FX_BTC_JPY now.
    update_position = self.nprivc.query_open_interest_summary("FX_BTC_JPY")
    update_position.msg = BitflyerPrivateParser.parse_position(update_position.msg)

    update_margin = self.nprivc.query_margin_status()
    update_margin.msg = BitflyerPrivateParser.parse_margin(update_margin.msg)

    update_position.msg.each_margin_usage.extend([update_margin.msg])
    return update_position

  def query_fills_impl(self, product):
    update = self.nprivc.query_list_executions(product.native_symbol,
                                               count=100,
                                               before=None,
                                               after=None)
    update.msg = BitflyerPrivateParser.parse_fills(update.msg, product)
    return update

  def query_list_orders_impl(self, product):
    update = self.nprivc.query_list_orders(
        product.native_symbol,
        child_order_acceptance_id=None,
        child_order_state='ACTIVE',
        count=100,
        before=None,
        after=None,
    )
    update.msg = BitflyerPrivateParser.parse_orders(update.msg, product)
    return update

  def query_submit_impl(self, product, side, price, qty, minute_to_expire=1):
    update = self.nprivc.query_submit_order(product.native_symbol,
                                            'LIMIT',
                                            side,
                                            price,
                                            qty,
                                            minute_to_expire=minute_to_expire,
                                            time_in_force='GTC')
    return update

  def query_cancel_impl(self, product, order_id):
    update = self.nprivc.query_cancel_order(product.native_symbol, order_id)
    return update

  def query_cancel_product_impl(self, product):
    update = self.nprivc.query_cancel_product_orders(product.native_symbol)
    return update
